use std::{collections::{BTreeMap, HashSet}, env, fs::{File, create_dir_all, remove_dir_all, rename}, io::{BufRead, BufReader, BufWriter, Read, Write}, path::{Path, PathBuf, absolute}, process::{Command, Stdio}, thread::spawn};

use wtask_base::{app::{get_wtask_app_dir, get_wtask_data_dir, get_wtask_root_dir}, cmd::{create_file, file_name_remove_space, file_zip, run_cmd_pure}, error::{WError, WResult}, export::{Local, NaiveDateTime, log_debug, serde_from_reader, serde_to_string}, logs::{TIME_FORMAT_TIMELINE, log_format_str}, machine::random_token};

use crate::{
    file::utils::PathNode, msg_func_none, task::utils::{
        args::{WTaskSubmitMeta, WTaskSubmitMetaType},
        meta::{CmdArgs, CmdMeta, CmdRet, CmdRunningRet, TASK_RUN_LOG_END, TASK_RUN_LOG_PREFIX, TASK_RUN_META_FILE, TaskAddMeta, TaskMeta, TaskStatus, TaskSubmitMeta, WTASK_DISABLED_FILE, WTASK_INSTALL_FILES, WTaskMetaFile}
    }
};


static TASK_RUN_CMD_FILE: &str = "wtask.cmd.run.sh";

pub(super) fn get_podman_base() -> String {
    env::var("WTASK_PODMAN_BASE").unwrap_or("anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/anolisos".to_owned()).to_lowercase()
}



impl CmdMeta {
    #[allow(clippy::too_many_arguments)]
    pub fn new(id: String, tag: String, image: Option<String>, mut image_save: Option<String>, note: String, exe: String, cmd: Vec<String>, dir_result: PathBuf, envs: Option<BTreeMap<String, String>>) -> Self {
        #[allow(unused_mut)]
        let mut image = if let Some(image) = image {
            image
        } else {
            get_podman_base()
        };
        let envs = envs.unwrap_or_default();
        
        #[cfg(unix)]
        {
            if env::var("WTASK_FORCE_PODMAN").unwrap_or("0".to_owned()).eq("0") {
                image = "local".to_owned();
            }
        }
        
        if image.eq("local") && image_save.is_some() {
            image_save = None;
        }
        
        Self {
            id,
            tag,
            image,
            image_save,
            note,
            cmd,
            exe,
            status: TaskStatus::Waiting,
            time_create: Local::now(),
            time_done: None,
            dir_result,
            envs,
        }
    }

    pub(crate) fn load_from_dir(dir_result: &Path) -> WResult<Self> {
        let p = dir_result.join(TASK_RUN_META_FILE);
        let f = File::open(p)?;
        let r = BufReader::new(f);
        let meta = serde_from_reader(r)?;
        Ok(meta)
    }

    /// 除非指定日志文件，否则日志保存在状态文件内，返回新的日志文件路径
    pub(crate) fn save_to_file(&self) -> WResult<PathBuf> {
        // meta
        let p = self.dir_result.join(TASK_RUN_META_FILE);
        let data = serde_to_string(self).unwrap();
        create_file(&p, &data)?;
        // status
        let file_status_new_name = format!("{}{:?}{}", TASK_RUN_LOG_PREFIX, self.status, TASK_RUN_LOG_END);
        let file_status_new = self.dir_result.join(&file_status_new_name);
        let file_status_old_name = self.dir_result
            .read_dir()?
            .map(|x| {
                x.unwrap().path().file_name().unwrap().to_str().unwrap().to_string()
            })
            .find(| v | v.ends_with(TASK_RUN_LOG_END) && v.starts_with(TASK_RUN_LOG_PREFIX));
        if let Some(file_status_old_name) = file_status_old_name {
            if file_status_new_name != file_status_old_name {
                // 状态变了，移动旧状态
                let file_status_old = self.dir_result.join(file_status_old_name);
                rename(file_status_old, &file_status_new)?;
            }
        } else {
            create_file(&file_status_new, "")?;
        }
        Ok(file_status_new)
    }

    /// 运行
    /// 可以指定日志输出路径
    /// 返回receiver和运行的进行handle
    /// receiver为了实时读取日志，如果不需要可以直接丢弃
    pub(crate) fn run_cmd_task(mut self, msg_func: Box<dyn Fn(bool, String) + Send>) -> WResult<CmdRunningRet> {
        if self.cmd.is_empty() {
            msg_func(true, "No cmd found".to_owned());
            return Err(WError::Stop);
        }

        create_dir_all(&self.dir_result)?;

        let podman = !self.image.eq("local");
        let podman_name = format!("wtask_run_{}", &self.id.replace("-", "")[..12]);

        self.status = TaskStatus::Running;
        let file_log = self.save_to_file()?;

        let cmd_exe = self.cmd[0].clone();
        let cmd_str = self.cmd.join(" ");
        let cmd_force_podman = cmd_exe.contains(" ") || cmd_str.contains("\n");
        
        let (program, args, h_install) = if cmd_force_podman || podman {
            // 复杂的在podman里面运行，保证是bash
            let podman_image = if self.image.eq("local") {
                get_podman_base()
            } else {
                self.image.clone()
            };
            let dir_result_str = self.dir_result.to_string_lossy().to_string();
            let mut args = vec![
                "run".to_owned(),
                "-it".to_owned(),
                "--name".to_owned(),
                podman_name.to_owned(),
                "-w".to_owned(),
                dir_result_str.clone()
            ];
            let mut dirs = HashSet::new();
            let dir_src = get_wtask_root_dir().to_string_lossy().to_string();
            args.push("-e".to_owned());
            args.push(format!("WTASK_ROOT={dir_src}"));
            dirs.insert(dir_src);
            dirs.insert(dir_result_str);
            if cmd_exe.starts_with("/") || cmd_exe.starts_with("\\") {
                let p = PathBuf::from(cmd_exe);
                if p.is_absolute() && p.exists() {
                    dirs.insert(p.parent().unwrap().to_string_lossy().to_string());
                }
            }
            for (k, v) in self.envs.iter() {
                args.push("-e".to_owned());
                args.push(format!("{k}={v}"));
                for i in v.split("@@@") {
                    if !(i.starts_with("/") || i.starts_with("\\")) {
                        continue;
                    }
                    let p = PathBuf::from(i);
                    if p.is_absolute() && p.exists() {
                        dirs.insert(p.parent().unwrap().to_string_lossy().to_string());
                    }
                }
            }
            for d in dirs {
                args.push("-v".to_owned());
                args.push(format!("{d}:{d}"));
            }

            if self.image_save.is_none() {
                args.push("--rm".to_owned());
            }
            
            args.extend(vec![
                podman_image,
                "sh".to_owned()
            ]);
            
            let cmd_strs = if cmd_force_podman {
                // write file
                let p = self.dir_result.join(TASK_RUN_CMD_FILE);
                let mut f = File::create(&p)?;
                f.write_all(cmd_str.as_bytes())?;
                vec![p.to_string_lossy().to_string()]
            } else {
                vec!["-c".to_owned(), cmd_str]
            };
            args.extend(cmd_strs);
            
            // check image first
            let has_image = run_cmd_pure(vec!["podman", "images", &self.image], None, None)?.contains(&self.image);
            let h = if (!has_image) && self.image.starts_with("wtask_") {
                let task_id_version = self.image.strip_prefix("wtask_").unwrap();
                let task_id_version_dir = task_id_version.replacen(":", ".", 1);
                let task_id_version_display = task_id_version.replacen(":", "@", 1);
                let dir_task_src = get_wtask_data_dir("task", false)?.join(&task_id_version_dir);
                let file_install = dir_task_src.join(WTASK_INSTALL_FILES[3]).to_string_lossy().to_string();
                let v = Self::new(
                    format!("{}.install", task_id_version_dir),
                    "任务安装".to_owned(),
                    None,
                    None,
                    format!("task {} install", task_id_version_display),
                    task_id_version_display,
                    vec![file_install],
                    dir_task_src,
                    None
                ).run_cmd_task(Box::new(msg_func_none))?;
                Some(v.handle)
            } else {
                None
            };
            ("podman".to_owned(), args, h)
        } else {
            (cmd_exe, self.cmd.clone().into_iter().skip(1).collect::<Vec<String>>(), None)
        };

        let dir_result = self.dir_result.clone();
        let id = self.id.clone();
        let exe = self.exe.clone();
        
        
        let handle = spawn(move || {
            if let Some(h_install) = h_install {
                h_install.join().map_err(|_| WError::Join)??;
            }

            let f_out = File::create(&file_log)?;
            let mut f_out = BufWriter::new(f_out);

            let mut child = Command::new(&program);
            if !args.is_empty() {
                child.args(&args);
            }

            // env
            if (!self.envs.is_empty()) && (!podman) {
                child.envs(&self.envs);
            }
            
            // log
            let mut child = child
                .current_dir(&self.dir_result)
                .stdout(Stdio::piped()) // 捕获标准输出
                .stderr(Stdio::piped())
                .spawn()?; // 启动子进程


            log_debug!("Run {:?} {:?} ...", program, args);

            if let Some(stdout) = child.stdout.take() {
                let mut reader = BufReader::new(stdout);
                let mut buf = String::new();
                while let Ok(n) = reader.read_line(&mut buf) {
                    if n == 0 {
                        break;
                    }
                    let log_temp = log_format_str(buf.trim_end());
                    let _ = f_out.write_all(log_temp.as_bytes());
                    msg_func(false, log_temp);
                    buf.clear();
                }
            }
            if let Some(stderr) = child.stderr.take() {
                let mut reader = BufReader::new(stderr);
                let mut buf = String::new();
                while let Ok(n) = reader.read_line(&mut buf) {
                    if n == 0 {
                        break;
                    }
                    let log_temp = log_format_str(buf.trim_end());
                    let _ = f_out.write_all(log_temp.as_bytes());
                    msg_func(true, log_temp);
                    buf.clear();
                }
            }
            let status = child.wait()?;

            // result
            if status.success() {
                self.status = TaskStatus::Success;
            } else {
                self.status = TaskStatus::Failed;
            }
            self.time_done = Some(Local::now());
            self.save_to_file()?;

            // save podman
            if let Some(image_save) = self.image_save.clone() {
                run_cmd_pure(vec!["podman", "commit", &podman_name, &image_save], None, None)?;
                run_cmd_pure(vec!["podman", "rm", &podman_name], None, None)?;
            }

            Ok::<_, WError>(self)
        });


        Ok(CmdRunningRet {
            basic: CmdRet {
                id,
                exe,
                dir_result,
            },
            handle
        })
    }
}




impl TaskMeta {
    pub(crate) fn new(dir: &Path) -> WResult<Self> {
        let (task_id, task_version) = dir.file_name().unwrap().to_str().unwrap().rsplit_once(".").unwrap();
        let file_doc = dir.join(WTASK_INSTALL_FILES[0]);
        let file_meta = dir.join(WTASK_INSTALL_FILES[1]);
        let file_cmd = dir.join(WTASK_INSTALL_FILES[2]);
        let enable = !dir.join(WTASK_DISABLED_FILE).exists();

        // doc/cmd
        let mut datas = [file_doc, file_cmd].into_iter().map(| v | {
            let mut f = File::open(v).unwrap();
            let mut data = String::new();
            f.read_to_string(&mut data).unwrap();
            data
        }).collect::<Vec<_>>();
        let cmd = datas.pop().unwrap();
        let doc = datas.pop().unwrap();
        
        let files = PathNode::new(dir, u8::MAX, None);

        // meta
        let f = File::open(file_meta)?;
        let r = BufReader::new(f);
        let meta =  serde_from_reader::<BufReader<File>, WTaskMetaFile>(r)?;
        if meta.name.trim().is_empty() {
            return Err(WError::Empty);
        }
        if meta.tags.is_empty() {
            return Err(WError::Empty);
        }
        
        Ok(Self {
            id: task_id.to_owned(),
            name: meta.name,
            version: task_version.to_owned(),
            doc,
            cmd,
            files,
            args: meta.args,
            tags: meta.tags,
            enable,
            dir: dir.to_string_lossy().to_string()
        })
    }


    fn parse_single(args_all: &mut BTreeMap<&str, &str>, args_res: &mut BTreeMap<String, String>, args_meta: &WTaskSubmitMeta, errs: &mut Vec<String>) -> WResult<()> {
        let mut children = BTreeMap::new();
        for (k, v) in args_meta.iter() {
            // 是否存在
            let value = if let Some(value) = args_all.remove(k.as_str()) {
                // 存在value
                // check
                let errs_temp = v.check(value)?;
                if !errs_temp.is_empty() {
                    errs.extend(errs_temp);
                    continue;
                }
                value.to_string()
            } else if let Some(value) = v.default() {
                // 不存在value但是有默认值
                value
            } else if v.required {
                // 不存在也没默认值，并且还必须
                errs.push(format!("Arg {} missing: 【{}】{}", k, v.name, v.doc));
                continue;
            } else {
                // 不存在也没默认值，还没必要，跳过
                continue;
            };
            // children
            if let WTaskSubmitMetaType::Select(arg_select) = &v.attrs
                && let Some(children_temp) = &arg_select.children
                    && let Some(vvs) = children_temp.kv.get(&value) {
                        for vv in vvs {
                            children.insert(vv.to_owned(), children_temp.args.get(vv).unwrap().to_owned());
                        }
                    }
            args_res.insert(k.to_owned(), value);
        }

        // children
        if !children.is_empty() {
            log_debug!("parse children ...");
            Self::parse_single(args_all, args_res, &children, errs)?;
        }
        Ok(())
    }

    
    pub(super) fn parse(&self, args: &[String]) -> WResult<(Vec<String>, BTreeMap<String, String>)> {
        let mut errs = Vec::new();
        let mut args_all = args.iter().filter_map(| v |{
            v.split_once("=")
        }).collect::<BTreeMap<&str, &str>>();
        let mut args_res = BTreeMap::new();
        Self::parse_single(&mut args_all, &mut args_res, &self.args, &mut errs)?;
        Ok((errs, args_res))
    }
}





impl CmdArgs {
    pub(crate) fn run_cmd_task(self, exe: String, msg_func: Box<dyn Fn(bool, String) + Send>) -> WResult<CmdRunningRet> {
        log_debug!("server {:?}", self);
        let id = random_token();
        let dir_result = if let Some(dir_result) = self.dir_result {
            PathBuf::from(dir_result)
        } else {
            get_wtask_data_dir("result", true)?.join(&id)
        };
        let envs = if self.envs.is_empty() {
            BTreeMap::new()
        } else {
            self.envs.iter()
                .filter(| &v | {
                    v.contains("=")
                })
                .map(| v |{
                    let t = v.split_once("=").unwrap();
                    (t.0.to_string(), t.1.to_string())
                })
                .collect::<BTreeMap<String, String>>()
        };
        let meta = CmdMeta::new(
            id,
            self.tag,
            Some(self.image.to_lowercase()),
            None,
            self.note,
            exe,
            self.cmd,
            dir_result,
            Some(envs)
        );
        
        meta.run_cmd_task(msg_func)
    }
}





impl TryInto<CmdArgs> for TaskSubmitMeta {
    type Error = WError;
    fn try_into(self) -> Result<CmdArgs, Self::Error> {
        log_debug!("prepare...");
        let dir_app = get_wtask_app_dir(&self.app, &self.version)?;
        let file_run = dir_app.join(WTASK_INSTALL_FILES[2]).to_string_lossy().to_string();
        let task = TaskMeta::new(&dir_app)?;
        // task status
        if !task.enable {
            return Err(WError::DataError(format!("Task disabled: {} {}", task.id, task.version)));
        }
        // check args
        log_debug!("check args...");
        let (arg_errs, args) = match task.parse(&self.args) {
            Ok(v) => v,
            Err(e) => (vec![e.to_string()], BTreeMap::new())
        };
        // args error
        if !arg_errs.is_empty() {
            return Err(WError::DataError(format!("Args error: \n{}\n\n{}", arg_errs.join("\n"), task)));
        }
        // run task
        log_debug!("run task...");
        let mut envs = args.into_iter().map(| (k, v) |{
            format!("wtask_{k}={v}")
        }).collect::<Vec<_>>();
        envs.push(format!("wtask_src_path={}", dir_app.to_str().unwrap()));
        
        let cmd_args = CmdArgs {
            note: self.note,
            image: format!("wtask_{}:{}", self.app, self.version),
            tag: self.tag,
            envs,
            cmd: vec![file_run],
            dir_result: self.dir_result,
        };
        Ok(cmd_args)
    }
}






pub(crate) fn task_local_simple_check<M: FnMut(bool, String)>(dir: &Path, mut msg_func: M) -> WResult<bool> {
    let task_id = dir.file_name().unwrap().to_str().unwrap();
    msg_func(false, format!("{} Checking ...", task_id));

    // file_exist
    msg_func(false, "Checking files if exist ...".to_owned());
    let mut is_error = false;
    for &file in WTASK_INSTALL_FILES.iter() {
        let p = dir.join(file);
        if p.exists() {
            msg_func(false, format!("File {} exist", file));
            continue;
        }
        msg_func(true, format!("File {} not exist", file));
        is_error = true;
    }
    if is_error {
        return Ok(true);
    }

    // file meta
    msg_func(false, "Checking meta file ...".to_owned());
    let file_meta = dir.join(WTASK_INSTALL_FILES[1]);
    let f = File::open(file_meta)?;
    let r = BufReader::new(f);
    match serde_from_reader::<BufReader<File>, WTaskMetaFile>(r) {
        Ok(meta) => {
            // name
            if meta.name.trim().is_empty() {
                is_error = true;
                msg_func(true, "Name empty".to_owned());
            } else {
                msg_func(false, "Name check complete".to_owned());
            }
            // tags
            if meta.tags.is_empty() {
                is_error = true;
                msg_func(true, "Tags empty".to_owned());
            } else {
                msg_func(false, "Tags check complete".to_owned());
            }
            // args
            for (_, v) in meta.args.iter() {
                match v.parse() {
                    Ok(errs) => {
                        if errs.is_empty() {
                            msg_func(false, format!("Args {} check complete", v.name));
                        } else {
                            is_error = true;
                            msg_func(true, format!("Args {} found {} errors", v.name, errs.len()));
                        }
                    },
                    Err(e) => {
                        is_error = true;
                        msg_func(true, format!("Args {} check error: {}", v.name, e));
                    }
                }
            }
        },
        Err(e) => {
            is_error = true;
            msg_func(true, format!("{task_id} Arg file parse error: {e}"));
        }
    }
    msg_func(false, format!("{} Task syntax check complete, {}", task_id, if is_error { "error" } else { "success" }));
    Ok(is_error)
}



impl TaskAddMeta {
    pub fn zip_task_files<M: FnMut(bool, String) + Send + Sync>(&self, mut msg_func: M) -> WResult<PathBuf> {
        let dir = if self.path.starts_with("http") || self.path.starts_with("git") {
            let task_id = self.path.rsplit_once("/").unwrap().1.replace(".git", "");
            let dir_result = get_wtask_data_dir("temp", true)?;
            // git clone
            if let Err(e) = run_cmd_pure(
                vec!["git", "clone", "--depth", "1", &self.path],
                None,
                Some(&dir_result)
            ) {
                return Err(WError::DataError(format!("git clone error: {e}")));
            };
            let _ = remove_dir_all(dir_result.join(&task_id).join(".git"));
            dir_result.join(task_id)
        } else {
            absolute(PathBuf::from(&self.path))?
        };

        if self.path.ends_with(".zip") {
            return Ok(dir);
        }

        if !dir.is_dir() {
            return Err(WError::DataError(format!("Path need dir: {dir:?}")));
        }

        msg_func(false, "Task checking ...".to_owned());
        let is_error = match task_local_simple_check(&dir, &mut msg_func) {
            Ok(t) => t,
            Err(e) => {
                return Err(WError::DataError(format!("{e}")));
            }
        };
        if is_error {
            return Err(WError::DataError("task local simple check error".to_string()));
        }
        // zip data
        let dir_name = dir.file_name().unwrap().to_str().unwrap();
        let version = {
            let version = if let Some(version) = &self.version {
                let t = NaiveDateTime::parse_from_str(version, TIME_FORMAT_TIMELINE);
                if t.is_ok() {
                    Some(version)
                } else {
                    None
                }
            } else {
                None
            };
            if let Some(version) = version {
                version.to_owned()
            } else {
                Local::now().format(TIME_FORMAT_TIMELINE).to_string()
            }
        };
        let file_temp = dir.parent().unwrap().join(format!(
            "{dir_name}.{version}.zip"
        ));
        msg_func(false, "Task ziping ...".to_owned());
        if let Err(e) = file_zip(&dir, &file_temp) {
            return Err(WError::DataError(format!("zip error: {file_temp:?} {e}")));
        };
        msg_func(false, format!("Task zipped: {:?}", file_temp));
        Ok(file_temp)
    }
}



pub(crate) fn archv_task_result(dir_task: &Path) -> WResult<PathBuf> {
    let month_task = dir_task.parent().unwrap().file_name().unwrap().to_str().unwrap();
    let meta_temp = CmdMeta::load_from_dir(dir_task)?;
    let name_temp = file_name_remove_space(&meta_temp.note);
    let dir_name = if name_temp.is_empty() {
        meta_temp.id
    } else {
        name_temp
    };
    let mut dir_res = get_wtask_data_dir("archv", false)?;
    dir_res = dir_res.join(month_task);
    create_dir_all(&dir_res)?;
    dir_res = dir_res.join(format!("{}.{}", meta_temp.time_create.format("%Y%m%d%H%M%S"), dir_name));
    rename(dir_task, &dir_res)?;
    Ok(dir_res)
}